package crmdna.client.member;

import static crmdna.common.OfyService.ofy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.googlecode.objectify.VoidWork;
import com.googlecode.objectify.Work;

import crmdna.client.contactdetail.ContactDetail.ContactDetailProp;
import crmdna.client.group.Group;
import crmdna.client.member.InteractionCore.InteractionProp;
import crmdna.client.member.InteractionCore.InteractionType;
import crmdna.client.member.MemberCore.MemberProp;
import crmdna.client.program.CheckIn;
import crmdna.client.program.CheckIn.CheckInStatusProp;
import crmdna.client.program.Program;
import crmdna.client.program.Program.ProgramProp;
import crmdna.client.programtype.ProgramType;
import crmdna.client.programtype.ProgramType.ProgramTypeProp;
import crmdna.client.sequence.Sequence;
import crmdna.client.sequence.Sequence.SequenceType;
import crmdna.client.user.User;
import crmdna.client.user.User.Action;
import crmdna.client.user.User.ResourceType;
import crmdna.client.user.UserCore;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.Utils;
import crmdna.crmdna.Client;

public class Member {	
					
	public static MemberProp create(String client, long groupId, ContactDetailProp contact, String login) {
		Client.ensureValidClient(client);			
		UserCore.ensureValidUser(client, login);
		
		//ensure valid center
		Group.safeGet(client, groupId);	
		
		MemberProp memberProp = MemberCore.create(client, groupId, contact);									
		return memberProp;				
	}
	
	public static List<UnverifiedProgramProp> getUnverifiedPrograms(String client, 
			long memberId) {
		List<UnverifiedProgramProp> props = new ArrayList<>();
		
		MemberCore.safeGet(client, memberId);
		
		UnverifiedProgramsEntity entity = ofy(client).load().type(UnverifiedProgramsEntity.class)
				.id(memberId).get();
		
		if (null == entity)
			return props;
		
		return entity.programs;
	}
	
	public static MemberProp saveUnverifiedPrograms(final String client, final long memberId, 
			final List<UnverifiedProgramProp> unverifiedPrograms, String login) {
		
		Client.ensureValidClient(client);
		User.ensureValidUser(client, login);
		
		MemberEntity memberEntity = Member.safeGet(client, memberId, login);
		
		//program type should be valid. cannot be any repetition
		Set<Long> programTypeIds = new HashSet<>();
		for (UnverifiedProgramProp up : unverifiedPrograms) {
			if (programTypeIds.contains(up.programTypeId)) {
				ProgramTypeProp prop = ProgramType.safeGet(client, up.programTypeId).toProp();
				Utils.throwIncorrectSpecException("Found multiple records for program type [" 
						+ prop.displayName + "]");
			}
			programTypeIds.add(up.programTypeId);
			
			Utils.ensureFormatYYYYMMDD(up.dateYYYYMM);
		}
		
		ProgramType.ensureValid(client, programTypeIds);
		
		Collections.sort(unverifiedPrograms);
		
		ofy(client).transact(new VoidWork() {
			
			@Override
			public void vrun() {
				UnverifiedProgramsEntity entity = ofy(client).load()
						.type(UnverifiedProgramsEntity.class).id(memberId).get();
				
				if (entity == null) {
					entity = new UnverifiedProgramsEntity();
					entity.memberId = memberId;
				}
				
				entity.programs = unverifiedPrograms;
				
				ofy(client).save().entity(entity).now();								
			}
		});
		
		MemberCore.populateDependantIndexFields(client, memberEntity);
		
		MemberProp memberProp = memberEntity.toProp();
		memberProp.unverifiedProgramProps = unverifiedPrograms;
		return memberProp;
	}
		
			
	public static class CheckInMemberProp {
		public long memberId;
		public String name;
		public String email;
		public String phoneNos;
		public boolean allow;
		public String notAllowingReason;
		public Set<Long> practiceIds;
	}
			
	public static List<Long> bulkCreateWithoutValidations(String client, List<ContactDetailProp> contactDetailProps, 
			long programId) {
				
		ProgramProp programProp = Program.safeGet(client, programId).toProp();
				
		//some elements in contactDetailProps can be null
		int numContacts = 0;
		for (ContactDetailProp c : contactDetailProps) {
			if (c != null)
				numContacts++;
		}
		
		List<Long> memberIds = new ArrayList<>();
		if (numContacts == 0)
			return memberIds;
		
		List<Long> ids = Sequence.getNext(client, SequenceType.MEMBER, numContacts);
		
		List<MemberEntity> toAdd = new ArrayList<>();
		
		long programTypeId = programProp.programTypeProp.programTypeId;
		List<Long> practiceIds = ProgramType.getPracticeIds(client, programTypeId);
		
		
		int i = 0;
		for (int index = 0; index < contactDetailProps.size(); index++) {
			ContactDetailProp c = contactDetailProps.get(index);
			if (c == null) {
				memberIds.add(null);
				continue;
			}
			
			long id = ids.get(i);
			
			MemberEntity memberEntity = new MemberEntity();			
			memberEntity.memberId = id;
			MemberCore.populateContactDetails(memberEntity, c);
			memberEntity.groupIds.add(c.centerId);
			memberEntity.programIds.add(programId);
			MemberCore.populateDependantIndexFieldsWithoutDBQuery(client, memberEntity);
			
			//other index fields
			memberEntity.programTypeIds.add(programTypeId);
			memberEntity.practiceIds.addAll(practiceIds);
			
			toAdd.add(memberEntity);
			
			memberIds.add(id);			
			i++;
		}
		
		ofy(client).save().entities(toAdd);
		return memberIds;				
	}
	
	public static void addProgramToMembers(String client, 
			List<MemberEntity> memberEntities, long programId) {
				
		Client.ensureValidClient(client);
		
		ProgramProp programProp = Program.safeGet(client, programId).toProp();	
											
		long programTypeId = programProp.programTypeProp.programTypeId;
		List<Long> practiceIds = ProgramType.getPracticeIds(client, programTypeId);
		
		List<MemberEntity> toSave = new ArrayList<>();
		for (int index = 0; index < memberEntities.size(); index++) {	
			
			MemberEntity entity = memberEntities.get(index);
			
			if (entity != null) {
				entity.programIds.add(programId);
				
				//other index fields
				entity.programTypeIds.add(programTypeId);
				entity.practiceIds.addAll(practiceIds);
				toSave.add(entity);				
			}			
		}
						
		ofy(client).save().entities(toSave);		
	}
	
	static void bulkUpdateWithoutValidations(String client,
			List<MemberEntity> memberEntities, List<ContactDetailProp> contacts, 
			List<Long> groupIds, long programId) {
		
		Client.ensureValidClient(client);
		
		ProgramProp programProp = Program.safeGet(client, programId).toProp();
		
		if (contacts.size() != groupIds.size()) 
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED, 
					"No of contacts do not match number of groups in bulkUpdateWithoutValidations");
		
		if (contacts.size() != memberEntities.size()) 
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED, 
					"No of contacts do not match number of memberentities in bulkUpdateWithoutValidations");
		
		//no validations will be done			
		
		long programTypeId = programProp.programTypeProp.programTypeId;
		List<Long> practiceIds = ProgramType.getPracticeIds(client, programTypeId);
		
		for (int i = 0; i < contacts.size(); i++) {
			
			MemberEntity memberEntity = memberEntities.get(i);
			Long groupId = null;
			if (i < groupIds.size()) {
				groupId = groupIds.get(i);
			}
			if (groupId == null)
				groupId = programProp.groupProp.groupId;
						
			MemberCore.populateContactDetails(memberEntities.get(i), contacts.get(i));
			memberEntity.groupIds.add(groupId);
			memberEntity.programIds.add(programId);
			
			//dependent fields
			MemberCore.populateDependantIndexFieldsWithoutDBQuery(client, memberEntity);
			memberEntity.programTypeIds.add(programTypeId);
			memberEntity.practiceIds.addAll(practiceIds);		
		}
		
		//async save
		ofy(client).save().entities(memberEntities);
	}
	
	public static MemberEntity safeGet(String client, long memberId, String login) {
		Client.ensureValidClient(client);			
		UserCore.ensureValidUser(client, login);
		
		return MemberCore.safeGet(client, memberId);
	}
					
	static MemberProp updateContactDetails(String client, long memberId, ContactDetailProp contact, 
			String login) {
		
		Client.ensureValidClient(client);		
		UserCore.ensureValidUser(client, login);
		
		MemberProp memberProp = MemberCore.updateContactDetails(client, memberId, contact);
		return memberProp;							
	}
	
	public static MemberProp addOrDeleteCenter(String client, long memberId, long groupId, 
			boolean add, String login) {
		
		Client.ensureValidClient(client);		
		UserCore.ensureValidUser(client, login);
		
		Group.safeGet(client, groupId);
		
		MemberProp memberProp = MemberCore.addOrDeleteGroup(client, memberId, groupId, add);
		return memberProp;		
	}
	
	public static MemberProp addOrDeleteProgram(String client, long memberId, long programId, 
			boolean add, String login) {
		
		//should be wrapped in a transaction
		Client.ensureValidClient(client);		
		UserCore.ensureValidUser(client, login);
		
		MemberEntity memberEntity = Member.safeGet(client, memberId, login);
		Program.safeGet(client, programId);
		
		boolean change;
		if (add)
			change = memberEntity.programIds.add(programId);
		else
			change = memberEntity.programIds.remove(programId);
		
		if (change) {
			memberEntity = MemberCore.populateDependantIndexFields(client, memberEntity);
			ofy(client).save().entity(memberEntity);
		}
							
		return memberEntity.toProp();		
	}
	
//	public static void bulkUploadProgramParticipants(String client, List<ContactDetailProp> contactDetails, 
//			List<Long> groupIds, long programId, String login) {
//		
//		Client.ensureValidClient(client);
//		
//		ProgramProp programProp = Program.safeGet(client, programId).toProp();
//		
//		//user should have permission
//		User.ensureAccess(client, login, ResourceType.PROGRAM, programProp.groupProp.groupId, Action.WRITE);
//		
//		if (contactDetails.size() != groupIds.size())
//			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED, 
//					"Number of groupIds [" + groupIds.size() + "] does not match number of contacts [" 
//							+ contactDetails.size() + "]");
//		
//		for (int i = 0; i < groupIds.size(); i++) {
//			if (groupIds.get(i) == null)
//				groupIds.set(i, programProp.groupProp.groupId);
//		}
//						
//		//ensure group ids are valid if not null
//		Group.ensureValidGroupIds(client, groupIds);
//		
//		//ensure all contacts are valid. 
//		ContactDetail.ensureValid(client, contactDetails);		
//		
//		//find out the list of emails that exist
//		List<ContactDetailProp> validEmailContactDetails = new ArrayList<>();
//		List<ContactDetailProp> invalidEmailcontactDetails = new ArrayList<>();
//		
//		Set<String> allEmails = new HashSet<>();
//		Set<String> allPhoneNumbers = new HashSet<>();
//		Map<String, ContactDetailProp> allEmailToContactDetails = new HashMap<>();
//		Map<String, Long> allEmailToGroupId = new HashMap<>();
//		for (int i = 0; i < contactDetails.size(); i++) {
//			ContactDetailProp c = contactDetails.get(i);	
//			
//			if (Utils.isValidEmailAddress(contactDetails.get(i).email)) {									
//				c.email = c.email.toLowerCase();
//				if (allEmails.contains(c.email))
//					throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS, 
//							"Duplicate email address [" + c.email + "] in record " + (i + 1) + "]");
//				
//				validEmailContactDetails.add(c);
//				allEmails.add(c.email);
//				allEmailToContactDetails.put(c.email, c);
//				allEmailToGroupId.put(c.email, groupIds.get(i));
//				
//			} else {
//				invalidEmailcontactDetails.add(c);
//				if (c.mobilePhone != null)
//					allPhoneNumbers.add(c.mobilePhone);
//				if (c.mobilePhone != null)
//					allPhoneNumbers.add(c.homePhone);
//				if (c.officePhone != null)
//					allPhoneNumbers.add(c.officePhone);
//			}
//		}
//		
//		//for contactdetails with email check how many are already present in db
//		Query<MemberEntity> emailQuery = ofy(client).load().type(MemberEntity.class)
//			.filter("email in", allEmails);
//		List<MemberEntity> validEmailExisting = emailQuery.list();
//		
//		Query<MemberEntity> phoneQuery = ofy(client).load().type(MemberEntity.class)
//			.filter("phoneNumbers in", allPhoneNumbers);
//		List<MemberEntity> invalidEmailExisting = phoneQuery.list();
//		
//		//valid email - existing
//		Map<String, ContactDetailProp> newEmailToContactDetails = new HashMap<>();
//		newEmailToContactDetails.putAll(allEmailToContactDetails);
//		for (MemberEntity memberEntity : validEmailExisting) {
//			//update the contact details
//			MemberCore.populateContactDetails(memberEntity, allEmailToContactDetails.get(memberEntity.email));
//			memberEntity.programIds.add(programId);
//			MemberCore.populateDependantIndexFields(client, memberEntity);
//			Long groupId = allEmailToGroupId.get(memberEntity.email);
//			if (null != groupId)
//				memberEntity.groupIds.add(allEmailToGroupId.get(memberEntity.email));
//			newEmailToContactDetails.remove(memberEntity.email);
//		}
//		
//		//save asynchronously
//		ofy(client).save().entities(validEmailExisting);
//		
//		//valid email - new
//		List<MemberEntity> validEmailNew = new ArrayList<>();
//		List<Long> memberIds = Sequence.getNext(client, SequenceType.MEMBER, newEmailToContactDetails.size());
//		int i = 0;
//		for (String email : newEmailToContactDetails.keySet()) {
//			ContactDetailProp c = newEmailToContactDetails.get(email);
//			MemberEntity memberEntity = new MemberEntity();
//			memberEntity.memberId = memberIds.get(i);
//			MemberCore.populateContactDetails(memberEntity, c);
//			Long groupId = allEmailToGroupId.get(email);
//			if (null == groupId)
//				groupId = programProp.groupProp.groupId;
//			memberEntity.groupIds.add(groupId);
//			memberEntity.programIds.add(programId);
//			MemberCore.populateDependantIndexFields(client, memberEntity);
//			
//			validEmailNew.add(memberEntity);
//		}
//		
//		//save asynchronously
//		ofy(client).save().entities(validEmailNew);
//				
//													
//		throw new APIException(Status.ERROR_NOT_IMPLEMENTED, "work in progress");
//	}
	
//	private static long getIndexByPhoneNumber(List<ContactDetailProp> contacts, String phoneNumber) {
//		for (int i = 0; i < contacts.size(); i++) {
//			if (phoneNumber.equals(contacts.get(i).mobilePhone))
//				return i;
//			if (phoneNumber.equals(contacts.get(i).officePhone))
//				return i;
//			if (phoneNumber.equals(contacts.get(i).homePhone))
//				return i;			
//		}
//		
//		return -1;
//	}
				
	static InteractionProp addInteraction(String client, long memberId, 
			String interaction, InteractionType interactionType, Date timestamp, String login) {
		
		Client.ensureValidClient(client);		
		long userId = UserCore.safeGet(client, login).toProp().userId;
		
		InteractionProp interactionProp = MemberCore.addInteraction(client, memberId, interaction, 
				interactionType, timestamp, userId);		
		return interactionProp;
	}
	
	static void deleteInteraction(final String client, final long memberId, final long interactionId,
			final String login) {			
		
		Client.ensureValidClient(client);		
		UserCore.ensureValidUser(client, login);
		safeGet(client, memberId, login);
		
		//need privilege to delete someone else's interaction
		long userId = UserCore.get(client, login).toProp().userId;
		InteractionProp interactionProp = InteractionCore.safeGet(client, interactionId).toProp();
		
		if (userId != interactionProp.userId)
			crmdna.client.user.User.ensureAccess(client, login, 
					ResourceType.INTERACTION, 0, Action.WRITE);
				
		MemberCore.deleteInteraction(client, memberId, interactionId);																
	}
	
	static InteractionProp updateInteraction(final String client, final long interactionId,
			final String interaction, final InteractionType interactionType,
			final Date timestamp, final String login) {			
		
		Client.ensureValidClient(client);		
		UserCore.ensureValidUser(client, login);
		
		//need privilege to delete someone else's interaction
		long userId = UserCore.get(client, login).toProp().userId;
		InteractionProp interactionProp = InteractionCore.safeGet(client, interactionId).toProp();
		
		if (userId != interactionProp.userId)
			crmdna.client.user.User.ensureAccess(client, login, 
					ResourceType.INTERACTION, 0, Action.WRITE);
			
		return InteractionCore.update(client, interactionId, interaction, 
				interactionType, timestamp, userId);															
	}
	
	static List<InteractionProp> getAllInteractions(String client, final long memberId, String login) {			
		
		Client.ensureValidClient(client);
		UserCore.ensureValidUser(client, login);	
				
		MemberProp prop = MemberCore.safeGet(client, memberId).toProp();
		return InteractionCore.get(client, prop.interactionIds);					
	}
			
	public static List<MemberProp> query(String client, String searchString, Set<Long> groupIds, 
			Set<Long> practiceIds, Integer maxResultSize, String login) {
		Client.ensureValidClient(client);		
		UserCore.ensureValidUser(client, login);
		
		List<MemberProp> memberProps = MemberCore.query(client, searchString, groupIds, 
				practiceIds, maxResultSize);				
		return memberProps;						
	}
	
	public static List<CheckInMemberProp> getMembersForCheckIn(String client, String searchStr, long programId, 
			int sessionDateYYYYMMDD, String login) {
		
		Client.ensureValidClient(client);
		User.ensureValidUser(client, login);
		
		if ((searchStr == null) || (searchStr.length() < 3))
			Utils.throwIncorrectSpecException("Atleast 3 chars required for search");	
		
		Program.safeGet(client, programId).toProp();
				
		//TODO: put an upper limit for results returned in member.query
		List<MemberProp> memberProps = Member.query(client, searchStr, null, 
				null, null, login);
		
		List<CheckInMemberProp> checkInMemberProps = new ArrayList<>();
		List<Long> memberIds = new ArrayList<>();
		for (MemberProp memberProp : memberProps) {			
			memberIds.add(memberProp.memberId);	
		}
		
		List<CheckInStatusProp> checkInStatus = CheckIn.getCheckInStatus(client, 
				programId, memberIds, sessionDateYYYYMMDD);
				
		for (int i = 0; i < memberIds.size(); i++) {	
			
			CheckInMemberProp checkInMemberProp = new CheckInMemberProp();
			
			MemberProp memberProp = memberProps.get(i);			
			checkInMemberProp.memberId = memberProp.memberId;
			checkInMemberProp.name = memberProp.contact.getName();
			checkInMemberProp.email = memberProp.contact.email;
			checkInMemberProp.phoneNos = memberProp.contact.getPhoneNos();
			checkInMemberProp.practiceIds = memberProp.practiceIds;
			checkInMemberProp.allow = true;
			
			CheckInStatusProp checkInStatusProp = checkInStatus.get(i);
			if (checkInStatusProp.checkedIn) {
				checkInMemberProp.allow = false;
				checkInMemberProp.notAllowingReason = checkInStatusProp.userFriendlyMessage;
			}
			
			checkInMemberProps.add(checkInMemberProp);
		}
		
		//TODO: handle the case where the program requires registration
		//add fields bool requireRegistration and Set<Long> registeredProgramIds in program entity				
		return checkInMemberProps;							
	}
 
	static List<MemberProp> searchByProgramType(String client, long groupId, List<Long> programTypeIds, 
			String login) {
		throw new APIException(Status.ERROR_NOT_IMPLEMENTED, "not yet implemented");
	}
	
	static List<MemberProp> searchByProgram(String client, List<Long> programIds, String login) {
		throw new APIException(Status.ERROR_NOT_IMPLEMENTED, "not yet implemented");
	}
	
	static List<MemberProp> searchByPractice(String client, Long groupId, 
			List<Long> practiceIds, String login) {
		throw new APIException(Status.ERROR_NOT_IMPLEMENTED, "not yet implemented");
	}
	
	static List<MemberProp> searchByPractice2(String client, Long groupId, 
			List<Long> includedPracticeIds, long excludePracticeId, String login) {
		throw new APIException(Status.ERROR_NOT_IMPLEMENTED, "not yet implemented");
	}
	
	public static List<Long> getMatchingMemberIds(String client, List<ContactDetailProp> contactDetailProps) {
		
		Client.ensureValidClient(client);
		
		if (contactDetailProps.size() == 0)
			return new ArrayList<>();
		
		for (ContactDetailProp contactDetailProp : contactDetailProps) {			
			if (contactDetailProp.email != null)
				contactDetailProp.email = contactDetailProp.email.toUpperCase();
		}	
				
		Map<String, Set<Integer>> emailMap = new HashMap<>();				
		Map<String, Set<Integer>> mobileMap = new HashMap<>();
				
		for (int index = 0; index < contactDetailProps.size(); index++) {
			ContactDetailProp c = contactDetailProps.get(index);
			if (c.email != null) {
				if (! emailMap.containsKey(c.email)) 
					emailMap.put(c.email, new HashSet<Integer>());
				
				Set<Integer> existing = emailMap.get(c.email);
				existing.add(index);				
			} else if (c.mobilePhone != null) {
				if (! mobileMap.containsKey(c.mobilePhone))
					mobileMap.put(c.mobilePhone, new HashSet<Integer>());
				
				Set<Integer> existing = mobileMap.get(c.mobilePhone);
				existing.add(index);
			} 
		}
		
		List<Long> memberIds = new ArrayList<>(contactDetailProps.size());		
					
		List<MemberEntity> emailMatches = ofy(client).load().type(MemberEntity.class)
				.filter("email in", emailMap.keySet()).list();
		
		List<MemberEntity> mobileMatches = ofy(client).load().type(MemberEntity.class)
				.filter("mobilePhone in", mobileMap.keySet()).list();
		
		for (MemberEntity me : emailMatches) {
			MemberProp mp = me.toProp();
			String memberName = mp.contact.firstName + " " + mp.contact.lastName;
			
			for (Integer index : emailMap.get(mp.contact.email)) {
				String particantName = contactDetailProps.get(index).firstName 
						+ contactDetailProps.get(index).lastName;
				
				if (Utils.closeEnough(memberName, particantName))
					memberIds.set(index, mp.memberId);
			}
		}
		
		for (MemberEntity me : mobileMatches) {
			MemberProp mp = me.toProp();
			String memberName = mp.contact.firstName + " " + mp.contact.lastName;
			
			for (Integer index : mobileMap.get(mp.contact.mobilePhone)) {
				if (memberIds.get(index) == null) {
					String particantName = contactDetailProps.get(index).firstName 
						+ contactDetailProps.get(index).lastName;
					
					if (Utils.closeEnough(memberName, particantName))
						memberIds.set(index, mp.memberId);
				}
			}
		}
		
		return memberIds;
	}
		 
	
	public static List<MemberProp> getAllForGroup(String client, long groupId, String login) {
		Client.ensureValidClient(client);
		Group.safeGet(client, groupId);
		UserCore.ensureValidUser(client, login);
		
		return MemberCore.getAllForGroup(client, groupId);
	}
}